ReactにCognitoでサインイン機能をつける
Cognitoで認証認可を実装する方法の整理
Cognitoを使用して認証認可をフロントエンドで実装する方法はいくつかあります。 自分はCognitoを使用するなら以下の3つが思いつきます。
- Amplifyの
Auth
モジュールの利用して認証画面を自作 - フロントエンドフレームワーク用のコンポーネントを使用する
- CognitoユーザープールのHostedUIを利用
上の方が低レベルのAPIが利用でき、カスタマイズなどはしやすいです。 ただ、特別なフローが不要であったり、デザインに関して細かい要求がない場合は下のものを使うのが楽だと思います。 利用できるコンポーネントとしてはReactやAngular、Vueがあります。
今回は「CognitoユーザープールのHostedUIを利用」で進めていきます。
準備
準備するのは以下の二つです。
- Reactアプリケーション
- Cognitoユーザープール
Reactの準備
今回はcreate-react-app
を利用してアプリケーションを作成します。
その後、amplifyパッケージをインストールします。
$ npx create-react-app amplify --template typescript $ cd amplify $ npm install --save aws-amplify
また、今回はローカルの開発でもHTTPSを使用する必要があります。
そのため、npmのpackage.json
のscripts
フィールドを以下のように設定して、ローカルでサーバーを立ててください。
"scripts": { "start": "HTTPS=true react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" },
Congitoユーザープールの準備
今回はユーザープールは以下の手順に従って作成します。 簡略化のために認証はGoogleのソーシャルサインインのみで行います。
ただ一点、注意点があります。 アプリクライアントを作成する際にクライアントシークレットを生成しない ようにしてください。 作成時に以下のチェックボックスを外してください。 これはOAuthの認証フローである「Authorization code grant」を使用するためです。
アプリクライアントの設定は以下のようになります。
実装
準備が終わったのでReactに認証機能を実装していきます。 最終的なコードは以下のようになります。
import { useEffect, useState } from 'react'; import Amplify, { Auth, Hub } from 'aws-amplify'; Amplify.configure({ Auth: { region: 'ap-northeast-1', userPoolId: 'ap-northeast-1_XXXXXXXXXX', userPoolWebClientId: 'XXXXXXXXXXXXXX', oauth: { domain: 'XXXXXXXXXXXXXX.auth.ap-northeast-1.amazoncognito.com', scope: ['openid'], redirectSignIn: 'https://localhost:3000/', redirectSignOut: 'https://localhost:3000/', responseType: 'code' } } }) const Example = () => { const [user, setUser] = useState<any | null>(null); useEffect(() => { Hub.listen('auth', ({ payload: { event, data } }) => { switch (event) { case 'signIn': case 'cognitoHostedUI': getUser().then(userData => setUser(userData)); break; case 'signOut': setUser(null); break; case 'signIn_failure': case 'cognitoHostedUI_failure': console.log('Sign in failure', data); break; } }); getUser().then(userData => setUser(userData)); }, []); const getUser = async () => { try { const userData = await Auth.currentAuthenticatedUser(); // デバッグ用 Auth.currentSession().then((data) => { console.log(`token: ${data.getIdToken().getJwtToken()}`); }); console.log(userData); return userData; } catch (e) { return console.log('Not signed in'); } } return user ? ( <div> <p>サインイン済み</p> <p>ユーザー名: {user.username}</p> <button onClick={() => Auth.signOut()}>Sign Out</button> </div> ) : ( <div> <p> サインインする </p> <button onClick={() => Auth.federatedSignIn()}>Sign In</button> </div> ); } export default Example
動かしてみる
$ npm start
ブラウザでhttps://localhost:3000
を開くと以下のような画面が出てきます。
サインインのボタンを押すとHostedUIにジャンプします。
Googleでの認証を行うと、localhost
にリダイレクトされます。
するとログインが完了しています。
Amplifyの設定
Amplify.configure({ Auth: { region: 'ap-northeast-1', userPoolId: 'ap-northeast-1_XXXXXXXXXX', userPoolWebClientId: 'XXXXXXXXXXXXXX', oauth: { domain: 'XXXXXXXXXXXXXX.auth.ap-northeast-1.amazoncognito.com', scope: ['openid'], redirectSignIn: 'https://localhost:3000/', redirectSignOut: 'https://localhost:3000/', responseType: 'code' } } })
AmplifyのAuthモジュールで利用する設定を書いています。
domain
はユーザープール作成の際に設定したドメイン名です。
responseType
はtoken
かcode
が選べます。
code
の場合は「Authorization code grant」のフローに従うことになります。
token
の場合は「Implict grant」となり、こちらは理由がなければ非推奨のようです。
イベントリスナーの設定
const [user, setUser] = useState<any | null>(null); useEffect(() => { Hub.listen('auth', ({ payload: { event, data } }) => { switch (event) { case 'signIn': case 'cognitoHostedUI': getUser().then(userData => setUser(userData)); break; case 'signOut': setUser(null); break; case 'signIn_failure': case 'cognitoHostedUI_failure': console.log('Sign in failure', data); break; } }); getUser().then(userData => setUser(userData)); }, []); const getUser = async () => { try { const userData = await Auth.currentAuthenticatedUser(); // 検証用 Auth.currentSession().then((data) => { console.log(`token: ${data.getIdToken().getJwtToken()}`); }); console.log(userData); return userData; } catch (e) { return console.log('Not signed in'); } }
Hub
でイベントリスナーを登録します。
今回はauth
イベントをハンドリングします。
サインインした際にはユーザーのデータをこのコンポーネントのステートに格納します。
サインアウトした際は削除します。
また、useEffect
の最後で最初にこのコンポーネントをロードした際、以前の認証情報が残っていれば、認証済みにしています。
getUser
はユーザーのデータを取得するためのヘルパー関数です。
非同期でユーザー情報が返ってくるのが注意点です。
サインインの方法によって、帰ってくるオブジェクトの型が大きく変わるので返り値の型はCognitoUser | Any
になっているようです。
検証用にトークンとユーザーデータを出力しています。
レンダリング
return user ? ( <div> <p>サインイン済み</p> <p>ユーザー名: {user.username}</p> <button onClick={() => Auth.signOut()}>Sign Out</button> </div> ) : ( <div> <p> サインインする </p> <button onClick={() => Auth.federatedSignIn()}>Sign In</button> </div> );
Auth.federatedSignIn
は引数としてIdPを取ります(例: google
など)
しかし、何も渡さないと、CognitoユーザープールのHostedUIを利用します。
ここでは、user
がnull
でない場合はサインイン済みとしています。
ユーザーデータとトークン
ここでのuserDataはCognitoUser
クラスのインスタンスです。
この中には先ほど利用したuearname
やユーザープールの情報、セッション情報などが含まれています。
JWTトークンはAuth.currentSession
から取得できるセッション情報から入手できます。
このトークンをリクエストと一緒に送信することで、認可に使用することができます。
サーバーサイドではこのトークンの有効性を検証すれば良いです。
感想
Cognitoを使用して認証・認可を実装することができました。 個人的なハマりどころは「Authorization code grant」を使用する際は、クライアントシークレットを作成しないというところです。 この点に気をつければ簡単に実装できると思います。